const FormData = require("form-data");
const fs = require("fs");
const path = require("path");
const url = require("url");
const uuid = require("uuid");
const qs = require("querystring");
const buffer = require("buffer");
const util = require("util");
const filesize = require("filesize");
const axios = require("axios").default.create({
  headers: {
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36 OPR/51.0.2830.55"
  }
});
const rs = require("randomstring");
const base64 = require("js-base64");
base64.Base64.extendString();
const crypto = require("crypto");
const Toolbox = require("../Toolbox");

const UID = "4874820"; //假的UID

function getTimeString() {
  let d = new Date;
  let month = d.getMonth() + 1;
  if (month < 10) {
    month = '0' + month;
  }
  let dayOfMonth = d.getDate();
  if (dayOfMonth < 10) {
    dayOfMonth = '0' + dayOfMonth;
  }
  let hour = d.getHours();
  if (hour < 10) hour = '0' + hour;
  let minute = d.getMinutes();
  if (minute < 10) minute = '0' + minute;
  let second = d.getSeconds();
  if (second < 10) second = '0' + second;
  /** @todo*/
  return `${d.getFullYear()}${month}${dayOfMonth}${hour}${
    minute
    }${second}`
}

function createPiankeSign(timestamp) {
  return crypto.createHash("md5").update(`${UID}:${timestamp}`).digest("hex").toUpperCase()
}

function createPiankeAuthorization(timestamp) {
  return base64.Base64.btoa(`${UID}:${timestamp}`);
}

/**
 *
 *
 * @param {String} filename
 * @returns {Promise<{ok:Boolean,msg:String,token:string}>}
 */
function getToken(filename) {
  return new Promise(resolve => {
    let timestamp = getTimeString();
    let ext = path.extname(filename);
    ext = ext ? ext : ".bin";
    let after_filename = `${uuid()}${ext}`
    axios.get(`http://pianke.me/version5.0/additional/getUploadToken.php`, {
      params: {
        filename: after_filename,
        sig: createPiankeSign(timestamp)
      },
      headers: {
        Authorization: createPiankeAuthorization(timestamp),
        'X-Requested-With': `XMLHttpRequest`,
        "referer": "http://pianke.me/pages/timeline/timeline.html"
      },
      validateStatus: s => s == 200,
      maxRedirects: 0
    }).then(axresp => {
      if (axresp.data['data'] && axresp.data['data']['token']) {
        let token = axresp.data['data']['token'];
        resolve({
          ok: true,
          msg: "ok",
          token: token
        });
      } else {
        throw new Error(`!axresp.data.data.token:${util.inspect(axresp.data)}`)
      }
    }).catch(axerr => {
      if (axerr.response) {
        resolve({
          ok: false,
          msg: `HTTP ${axerr.response.status} ${axerr.response.statusText} : ${axerr.response.data ?
            (axerr.response.data['message'] ? axerr.response.data['message'] : util.inspect(axerr.response.data)) :
            "!NO HTTP RESPONSE DATA"}`
        })
      } else {
        resolve({
          ok: false,
          msg: axerr.message ? axerr.message : util.inspect(axerr)
        });
      }
    })
  })
}

class PiankeAliyun {
  constructor() {
    /**@type {(full_path:string,parts_count:number,part_index:number)=>string} */
    this.default_part_name_func = (full_path, parts_count, part_index) => {
      parts_count = parseInt(parts_count);
      if (parts_count == 1) {
        return path.basename(full_path);
      } else {
        let max_length = String(parts_count).length;
        let str_index = String(part_index);
        while (str_index.length < max_length) {
          str_index = `0${str_index}`
        }
        return `${path.basename(full_path)}.sf-part${str_index}`
      }
    }
  }


  /**
   * 
   * @param {import("fs").ReadStream|String} stream 
   * @param {Strings} define_filename 
   * @param {number} size
   * @returns {Promise<{ok:Boolean,msg:String,url:String}>}
   */
  uploadStream(stream, size, define_filename = "test.png") {
    return new Promise(async resolve => {
      if (!size) {
        return resolve({
          ok: false,
          msg: `size is :${size}`
        })
      }
      let o_gettoken = await getToken(define_filename);
      if (!o_gettoken.ok) {
        return resolve({
          ok: false,
          msg: `error get token:${o_gettoken.msg}`
        })
      }
      let token = o_gettoken.token;
      let upload_url = `http://upload.media.aliyun.com/api/proxy/upload?${qs.stringify({
        "Authorization": token,
        "UserAgent": "ALIMEDIASDK_WORKSTATION"
      })}`;
      let parsed = url.parse(upload_url);
      let form = new FormData;
      form.append("dir", "");
      form.append("name", define_filename);
      form.append("size", size);
      form.append("content", stream, {
        filename: define_filename
      });
      let response_flag = false;
      form.on("end", () => {
        setTimeout(() => {
          if (!response_flag) {
            resolve({
              ok: false,
              msg: `[LATE RESPONSE] NO HTTP RESPONSE after form.end 60 seconds`
            })
          }
        }, 60 * 1000)
      })
      form.submit({
        host: parsed.host,
        path: parsed.path,
        protocol: parsed.protocol,
        headers: {
          "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36 OPR/52.0.2871.40"
        }
      }, async (err, resp) => {
        if (err) {
          return resolve({
            ok: false,
            msg: err.message
          })
        } else {
          response_flag = true;
          let buf = new buffer.Buffer("");
          resp.on("data", chunk => {
            buf += chunk;
          });
          resp.on("end", $ => {
            try {
              let obj = JSON.parse(buf);
              if (obj['url']) {
                resolve({
                  ok: true,
                  msg: "ok",
                  url: obj['url']
                })
              } else {
                throw new Error(`there is no 'url' attr in respose:${util.inspect(buf)}`)
              }
            } catch (e) {
              resolve({
                ok: false,
                url: "",
                msg: `error when try extract url :${e.message ? e.message : util.inspect(e)}`
              })
            }
          })
        }
      })

    })
  }

  /**
   *  @returns {Promise<{ok:Boolean,url:String,msg:String,file_size:String,speed:String}>}
   * @param {String} full_path 
   */
  getUploadSpeed(full_path) {
    return new Promise(async resolve => {
      let o_stats = await Toolbox.getStats(full_path);
      if ((!o_stats.ok) || (!o_stats.stats.isFile())) {
        return resolve({
          ok: false,
          msg: o_stats.msg ? o_stats.msg : `${full_path} is NOT File`
        })
      }
      let start_time = Date.now();
      console.log("start uploading...", path.basename(full_path));
      let o_up = await this.uploadStream(fs.createReadStream(full_path),
        o_stats.stats.size);
      let end_time = Date.now();
      if (!o_up.ok) {
        return resolve({
          ok: false,
          msg: o_up.msg,
        })
      } else {
        let bytes_per_second = o_stats.stats.size / ((end_time - start_time) / 1000);
        resolve({
          ok: true,
          msg: "ok",
          url: o_up.url,
          file_size: filesize(o_stats.stats.size),
          speed: filesize(bytes_per_second) + "/s"
        })
      }

    })
  }

  /**
   * 
   * @returns {Promise<{ok:Boolean,msg:String,files:Array<{name:String,url:String}>}>}
   * @param {string} full_file_path 
   * @param {number} size_limit 
   * @param {(full_path:string,parts_count:number,part_index:number)=>string} part_name_func 
   */
  uploadFile(full_file_path, size_limit = 499 * 1024 * 1024, part_name_func = this.default_part_name_func) {
    size_limit = parseInt(size_limit);
    if (size_limit < 0 || size_limit > 499 * 1024 * 1024) {
      size_limit = 499 * 1024 * 1024
    }
    return new Promise(async resolve => {
      let o_stats = await Toolbox.getStats(full_file_path);
      if (!o_stats.ok) {
        return resolve({
          ok: false,
          msg: `fail to get stats of ${file_full_path} : ${o_stats.msg}`
        })
      }
      if (!o_stats.stats.isFile()) {
        return resolve({
          ok: false,
          msg: ` ${file_full_path} : it is not a file`
        })
      }
      let CHUNK_MAX_SIZE = size_limit;
      let CHUNKS_COUNT = Math.ceil(o_stats.stats.size / CHUNK_MAX_SIZE);
      /**@type {Array<{name:String,url:String}>} */
      let files = [];
      for (let i = 0; i < CHUNKS_COUNT; i++) {
        let start = i * CHUNK_MAX_SIZE;
        let end = start + CHUNK_MAX_SIZE - 1;
        if (end >= o_stats.stats.size) {
          end = o_stats.stats.size - 1;
        }
        let o_upChunk = await this.uploadStream(
          fs.createReadStream(full_file_path, {
            start: start,
            end: end
          }),
          end - start + 1,
          part_name_func(full_file_path, CHUNKS_COUNT, i)
        )
        if (!o_upChunk.ok) {
          return resolve({
            ok: false,
            msg: `error when upload part ${i + 1}:${o_upChunk.msg}`
          })
        }
        files.push({
          name: part_name_func(full_file_path, CHUNKS_COUNT, i),
          url: o_upChunk.url
        })
      }
      resolve({
        ok: true,
        msg: "ok",
        files: files
      })
    })
  }

  /**
   *
   * @returns {Promise<{ok:Boolean,errors:string[],toHTML:()=>string,files:Array<{path:string,chunks:Array<{url:string,name:string}>  }>   }>}
   * @param {String} full_path_of_dir
   * @param {number} [size_limit=499 * 1024 * 1024]
   * @param {String} base_dir
   * @param {(full_path:string,parts_count:number,part_index:number)=>string} [part_name_func=this.default_part_name_func]
   * @memberof Pianke7Niu
   */
  safelyUploadDir(full_path_of_dir, size_limit = 499 * 1024 * 1024, base_dir = full_path_of_dir, part_name_func = this.default_part_name_func) {
    return new Promise(async resolve => {
      let o_stats = await Toolbox.getStats(full_path_of_dir);
      if (!o_stats.ok) {
        let err = `fail to get stats of ${full_path_of_dir} : ${o_stats.msg}`
        return resolve({
          ok: false,
          errors: [err],
          toHTML: () => `<font color="red">${err}</font>`
        })
      }
      if (!o_stats.stats.isDirectory()) {
        let err = ` ${full_path_of_dir} : it is not a folder`
        return resolve({
          ok: false,
          errors: err,
          toHTML: () => `<font color="red">${err}</font>`
        })
      }
      let safeList = await Toolbox.safeListDir(full_path_of_dir);
      /**@type {string[]} */
      let errors = [];
      /**@type {Array<{path:string,chunks:Array<{url:string,name:string}> } >} */
      let files = [];
      for (let item of safeList) {
        if (item.stats.isDirectory()) {
          let o_uploadChildFolder = await this.safelyUploadDir(item.full_path, size_limit, base_dir, part_name_func);
          errors = errors.concat(o_uploadChildFolder.errors);
          files = files.concat(o_uploadChildFolder.files);
        } else if (item.stats.isFile()) {
          let o_upFile = await this.uploadFile(item.full_path, size_limit, part_name_func);
          if (!o_upFile.ok) {
            errors.push(`failed to upload ${path.relative(base_dir, item.full_path)}:${o_upFile.msg}`)
          } else {
            files.push({
              path: path.relative(base_dir, item.full_path),
              chunks: o_upFile.files.map(o => {
                return {
                  url: o.url,
                  name: o.name
                }
              })
            })
          }
        }
      }
      resolve({
        ok: true,
        errors: errors,
        files: files,
        toHTML: () => {
          let error_html = errors.map(e => `<font color="#BF134C">${e}</font>`).join("<hr>");
          let files_html = files.map(file => {
            if (file.chunks.length == 1) {
              return `<a href="${file.chunks[0].url}"><font color="#177971"><u>${file.path}</u></font></a>`
            } else {
              return file.chunks.map(chunk => {
                return `<a href="${chunk.url}"><font color="#21a821"><u>${
                  path.join(path.dirname(file.path), chunk.name)
                  }</u></font></a>`
              })
            }
          }).join("<br>");
          return `${error_html ? `<blockquote><h4><font color="#BF341C">ERROR LOGS:</font></h4>${error_html}</blockquote>\n` : ""}` +
            `<blockquote><h4><font color="#5CAD5C">LINKS :</font></h4><p><blockquote>${files_html}</blockquote></p></blockquote>`

        }
      })

    })
  }



}

module.exports = {
  PiankeAliyun: PiankeAliyun
}